﻿package mlc.haxe;
import haxe.Log;
import haxe.PosInfos;
import mlc.haxe.input.command.Command;
import mlc.haxe.input.command.DyalicCommand;
import mlc.haxe.input.command.ExecutableCommand;
import mlc.haxe.input.command.MonadicCommand;
import mlc.haxe.input.command.NullCommand;
import mlc.haxe.input.command.RawCommand;
import mlc.haxe.input.command.VoidCommand;
import mlc.haxe.input.ConsoleInputSource;
import mlc.haxe.input.formatting.StringFormatter;
import mlc.haxe.input.NullInputSource;
import mlc.haxe.input.formatting.InputFormatter;
import mlc.haxe.input.parsing.InputParser;
import mlc.haxe.output.ConsoleOutputHandler;
import mlc.haxe.output.NullOutputHandler;
import mlc.haxe.output.preprocessing.BasicTracePreprocessor;
import mlc.haxe.output.preprocessing.ConsoleTracePreprocessor;

/**
 * A console that can be used for many different things. It can be used, for instance, to store and show error and debug
 * messages. It can also be used to change the state of an application or game at runtime.
 */
class Console {
	/**
	 * The aliases of commands.
	 */
	private var aliases:Hash<String>;
	/**
	 * The commands of the console.
	 */
	private var commands:Hash<ExecutableCommand>;
	/**
	 * The input parser that will be used to parse all the input.
	 */
	private var inputParser:InputParser;
	/**
	 * The output handler used by this console.
	 */
	private var outputHandler:ConsoleOutputHandler;
	/**
	 * The trace preprocessor used by this console.
	 */
	private var tracePreprocessor:ConsoleTracePreprocessor;
	/**
	 * Creates a new console. You may pass an output handler, an input source, and a trace preprocessor. If you do not provide an
	 * output handler, everything the console outputs will be ignored (making the clear method, the trace method and the write
	 * method useless). If you do not provide an input source, the console will not receive any input (making all command related
	 * methods useless). If no trace preprocessor is provided, a basic trace preprocessor will be used.
	 */
	public function new(?outputHandler:ConsoleOutputHandler, ?inputSource:ConsoleInputSource, ?tracePreprocessor:ConsoleTracePreprocessor):Void {
		// Store the output handler, using a null output handler if no output handler is provided.
		this.outputHandler =
			if (null == outputHandler) {
				new NullOutputHandler();
			} else {
				outputHandler;
			}
		// Store the trace preprocessor, using a basic trace preprocessor if no trace preprocessor is provided.
		this.tracePreprocessor =
			if (null == tracePreprocessor) {
				new BasicTracePreprocessor();
			} else {
				tracePreprocessor;
			}
		// Call the handle input method whenever the input source dispatches that it has input, but do this only if an input source
		// is provided.
		if (null != inputSource) {
			inputSource.inputReceivedSignaler.bind(handleInput);
		}
		// Setup the hash of aliases.
		aliases = new Hash();
		// Setup the hash of commands.
		commands = new Hash();
		// Add the default clear command.
		addVoidCommand("clear", this.outputHandler.clearOutput);
		// Add the default addAlias and removeAlias commands.
		var stringFormatter:StringFormatter = new StringFormatter();
		addDyalicCommand("addAlias", addAlias, stringFormatter, stringFormatter);
		addDyalicCommand("removeAlias", removeAlias, stringFormatter, stringFormatter);
		// Create an input parser.
		inputParser = new InputParser(this);
	}
	/**
	 * Adds an alias for a command, such as "pr" for "post results". You can add an alias before adding the actual command. If
	 * you add an alias for a command that doesn't exist, the alias will still be created. If the console already has an alias
	 * with the passed name, calling this method has no effect.
	 */
	public function addAlias(name:String, command:String):Void {
		if (false == aliases.exists(name)) {
			// If the passed command is actually an alias, point to the command rather than the alias.
			if (aliases.exists(command)) {
				aliases.set(name, aliases.get(command));
			} else {
				aliases.set(name, command);
			}
		}
	}
	/**
	 * Adds a command that accepts two arguments. Those arguments are formatted by the passed input formatters. The added command
	 * can be executed by sending input to the console that starts with the name of the command. If the console already has a
	 * command or an alias with the passed name, calling this method has no effect.
	 */
	public function addDyalicCommand<Datatype1, Datatype2>(name:String, method:Datatype1 -> Datatype2 -> Void, firstInputFormatter:InputFormatter<Datatype1>, secondInputFormatter:InputFormatter<Datatype2>):Command {
		// If a command with the passed name already exists, don't proceed. Return a null command rather than actually return null,
		// because the client might try to use the returned command.
		if (commands.exists(name) || aliases.exists(name)) {
			return new NullCommand();
		}
		var result:DyalicCommand<Datatype1, Datatype2> = new DyalicCommand<Datatype1, Datatype2>(method, firstInputFormatter, secondInputFormatter, commands.remove, name);
		commands.set(name, result);
		return result;
	}
	/**
	 * Adds a command that accepts one argument. That argument is formatted by the passed input formatter. The added command can
	 * be executed by sending input to the console that starts with the name of the command. If the console already has a command
	 * with the passed name, calling this method has no effect.
	 */
	public function addMonadicCommand<Datatype>(name:String, method:Datatype -> Void, inputFormatter:InputFormatter<Datatype>):Command {
		// If a command with the passed name already exists, don't proceed. Return a null command rather than actually return null,
		// because the client might try to use the returned command.
		if (commands.exists(name)) {
			return new NullCommand();
		}
		var result:MonadicCommand<Datatype> = new MonadicCommand<Datatype>(method, inputFormatter, commands.remove, name);
		commands.set(name, result);
		return result;
	}
	/**
	 * Adds a command that accepts any number of arguments, and formats/processes those arguments itself. The added command can
	 * be executed by sending input to the console that starts with the name of the command. If the console already has a command
	 * with the passed name, calling this method has no effect.
	 */
	public function addRawCommand(name:String, method:Array<String> -> Void):Command {
		// If a command with the passed name already exists, don't proceed. Return a null command rather than actually return null,
		// because the client might try to use the returned command.
		if (commands.exists(name)) {
			return new NullCommand();
		}
		var result:RawCommand = new RawCommand(method, commands.remove, name);
		commands.set(name, result);
		return result;
	}
	/**
	 * Adds a command that accepts no arguments. The added command can be executed by sending input to the console that starts
	 * with the name of the command. If the console already has a command with the passed name, calling this method has no
	 * effect.
	 */
	public function addVoidCommand(name:String, method:Void -> Void):Command {
		// If a command with the passed name already exists, don't proceed. Return a null command rather than actually return null,
		// because the client might try to use the returned command.
		if (commands.exists(name)) {
			return new NullCommand();
		}
		var result:VoidCommand = new VoidCommand(method, commands.remove, name);
		commands.set(name, result);
		return result;
	}
	/**
	 * Clears the output. The user might not be able to access the information that was written prior to calling this method.
	 * However, this is not guaranteed, as it depends on the implementation of the passed output handler.
	 */
	public inline function clear():Void {
		outputHandler.clearOutput();
	}
	private function handleInput(input:String):Void {
		var arguments:Array<String> = inputParser.parseInput(input);
		var commandName:String = arguments.shift();
		if (aliases.exists(commandName)) {
			commandName = aliases.get(commandName);
		}
		if (commands.exists(commandName)) {
			commands.get(commandName).execute(arguments, writeError);
		} else {
			writeError("Unknown command \"" + commandName + "\"");
		}
	}
	/**
	 * Overrides haxe.Log.trace with the trace method of this console instance. Whenever a trace call is made after this method
	 * is called, the trace call will be routed to this console instance.
	 */
	public function redirectTraces():Void {
		Log.trace = trace;
	}
	/**
	 * Removes an alias. If the console has no alias with the passed name, or that alias points to a different command than the
	 * one passed now, calling this method has no effect.
	 */
	public function removeAlias(name:String, command:String):Void {
		if (aliases.exists(name) && (command == aliases.get(name) || (aliases.exists(command) && aliases.get(command) == aliases.get(name)))) {
			aliases.remove(name);
		}
	}
	/**
	 * Removes a command. If the console has no command with the passed name, or that command has a different method than the one
	 * passed now, calling this method has no effect.
	 */
	public function removeDyalicCommand<Datatype1, Datatype2>(name:String, method:Datatype1 -> Datatype2 -> Void):Void {
		// If a command with the passed name exists, and the equality check passes, remove the command.
		if (commands.exists(name) && commands.get(name).determineEquals(new DyalicCommand<Datatype1, Datatype2>(method, null, null, null, null))) {
			commands.remove(name);
		}
	}
	/**
	 * Removes a command. If the console has no command with the passed name, or that command has a different method than the one
	 * passed now, calling this method has no effect.
	 */
	public function removeMonadicCommand<Datatype>(name:String, method:Datatype -> Void):Void {
		// If a command with the passed name exists, and the equality check passes, remove the command.
		if (commands.exists(name) && commands.get(name).determineEquals(new MonadicCommand<Datatype>(method, null, null, null))) {
			commands.remove(name);
		}
	}
	/**
	 * Removes a command. If the console has no command with the passed name, or that command has a different method than the one
	 * passed now, calling this method has no effect.
	 */
	public function removeRawCommand(name:String, method:Array<String> -> Void):Void {
		// If a command with the passed name exists, and the equality check passes, remove the command.
		if (commands.exists(name) && commands.get(name).determineEquals(new RawCommand(method, null, null))) {
			commands.remove(name);
		}
	}
	/**
	 * Removes a command. If the console has no command with the passed name, or that command has a different method than the one
	 * passed now, calling this method has no effect.
	 */
	public function removeVoidCommand(name:String, method:Void -> Void):Void {
		// If a command with the passed name exists, and the equality check passes, remove the command.
		if (commands.exists(name) && commands.get(name).determineEquals(new VoidCommand(method, null, null))) {
			commands.remove(name);
		}
	}
	#if debug
	private function toString():String {
		return "[Console]";
	}
	#end
	/**
	 * Writes some value. The value is processed by the trace preprocessor that was passed in the constructor.
	 */
	public inline function trace(value:Dynamic, ?positionInformation:PosInfos):Void {
		tracePreprocessor.callOutputHandler(outputHandler, value, positionInformation);
	}
	private inline function writeError(message:String):Void {
		write(message, 0xF95A61);
	}
	/**
	 * Writes a string, preferably in the specified color. Whether the provided color is actually used or not depends on the
	 * implementation of the output handler. You can specify whether a linebreak is added after the string by setting the
	 * breakline argument. If you do not specify a value of the breakline argument, a breakline is placed.
	 */
	public inline function write(value:String, color:Int, ?breakline:Bool):Void {
		outputHandler.writeAdvancedString(value, color, null == breakline || true == breakline);
	}
}